#!/usr/bin/env escript
%% -*- erlang-indent-level: 4; indent-tabs-mode: nil -*-

-mode(compile).

%% tx pool interaction
-export([ delete_tx_from_pool/2
        , push_tx_to_pool/2
        , inspect_tx_in_pool/2
        , tx_pool_size/2
        , tx_pool_miner_gas_price/2
        , set_tx_pool_miner_gas_price/2
        ]).

%% peer pool interaction
-export([ list_connected_peers/2
        , list_verified_peers/2
        , list_unverified_peers/2
        , list_blocked_peers/2
        , add_peer/2
        , remove_peer/2
        , block_peer/2
        , unblock_peer/2
        ]).


%% See https://hexdocs.pm/argparse/
%% Argparse supports a hierarchical structure of commands and subcommands, each
%% with its own arguments. This spec (no command specified) describes the top level.
%%
%% We only provide the user-level arguments. The node connection arguments, which
%% are automatically provided by the script wrappers, are specified in aeu_ext_scripts.erl
spec() -> #{ commands => cmds()
                           }.

cmds() ->
    #{ "tx_pool" => tx_pool()
     , "peers" => peers()
     }.

tx_pool() ->
    #{ help => "Transaction pool commands"
    , commands => #{ "delete" => #{ help => "Delete a transaction from the pool"
                                  , handler => {fun delete_tx_from_pool/2,
                                                not_used}
                                  , arguments => [tx_hash_arg(),
                                                  timeout_arg()]} 
                   , "push" => #{ help => "Pushes a transaction in the pool"
                                , handler => {fun push_tx_to_pool/2,
                                              not_used}
                                , arguments => [#{ name => tx
                                                 , type => string
                                                 , help => "The encoded transaction"},
                                                #{ name => force
                                                 , long => "-force"
                                                 , type => boolean
                                                 , help => "If should push the transaction even if it has already been deleted"
                                                 , short => $f
                                                 , default => false},
                                                timeout_arg()]} 
                   , "size" => #{ help => "Provides the amount of transactions currently in the pool"
                                , handler => {fun tx_pool_size/2,
                                              not_used}
                                , arguments => [#{ name => show
                                                 , type => string
                                                 , long => "-show"
                                                 , help => "all | visited | not_visited"
                                                 , default => "all"},
                                                timeout_arg()]} 
                   , "inspect" => #{ help => "Inspects a transaction currently present in the pool"
                                   , handler => {fun inspect_tx_in_pool/2,
                                                 not_used}
                                   , arguments => [tx_hash_arg(),
                                                   timeout_arg()]}
                   , "miner_gas_price" =>
                      #{ help => "Handle tx_pool expectation for minimum gas_price"
                       , commands => #{ "get" => #{ helper => "Get the current miner_gas_price"
                                                  , handler => {fun tx_pool_miner_gas_price/2,
                                                                not_used}
                                                  , arguments => [timeout_arg()]
                                               }
                                      , "set" => #{ helper => "Set the minimum miner_gas_price. This does not impact transactions currently in the pool"
                                                  , handler => {fun set_tx_pool_miner_gas_price/2,
                                                                not_used}
                                                  , arguments => [#{ name => new_gas_price 
                                                                   , type => {int, [{min, 1000000}]}
                                                                   , help => "The new minimum gas price to be set. This does not impact transactions currently in the mempool."
                                                                   },
                                                                  timeout_arg()]
                                               }
                                      }
                       }
                } }.

peers() ->
    #{ help => "Peer pool commands"
     , commands =>
        #{ "list" =>
            #{ help => "Inpsect peers"
             , commands =>
                #{ "connected" =>
                    #{ help => "List connected peers"
                     , handler => {fun list_connected_peers/2, not_used}
                     , arguments => [#{ name => show
                                      , type => string
                                      , long => "-show"
                                      , help => "all | inbound | outbound"
                                      , default => "all"},
                                      count_arg(), timeout_arg()]}
                 , "verified" =>
                    #{ help => "List verified peers"
                     , handler => {fun list_verified_peers/2, not_used}
                     , arguments => [ count_arg()
                                    , timeout_arg()]}
                 , "unverified" =>
                    #{ help => "List unverified peers"
                     , handler => {fun list_unverified_peers/2, not_used}
                     , arguments => [ count_arg()
                                    , timeout_arg()]}
                 , "blocked" =>
                    #{ help => "List blocked peers"
                     , handler => {fun list_blocked_peers/2, not_used}
                     , arguments => [ count_arg()
                                    , timeout_arg()]}
                }
            }
        , "add" =>
            #{ help => "Add a peer"
              , handler => {fun add_peer/2, not_used}
              , arguments => [peer_address_arg(),
                              #{ name => trusted 
                              , long => "-trusted"
                              , type => boolean
                              , help => "If the peer is trusted"
                              , short => $c
                              , default => false},
                              timeout_arg()]}
        , "remove" =>
            #{ help => "Remove a peer"
              , handler => {fun remove_peer/2, not_used}
              , arguments => [ peer_id_arg(), timeout_arg()]}
        , "block" =>
            #{ help => "Block a peer"
              , handler => {fun block_peer/2, not_used}
              , arguments => [peer_address_arg(), timeout_arg()]}
        , "unblock" =>
            #{ help => "Unblock a peer"
              , handler => {fun unblock_peer/2, not_used}
              , arguments => [ peer_id_arg(), timeout_arg()]}
        }
    }.

tx_hash_arg() ->
    #{ name => tx_hash,
       type => string,
       help => "The hash of the transaction, Base58c encoded"}.

timeout_arg() ->
    #{ name => 'timeout'
     , short => $t
     , help => "Timeout when waiting for response from the node"
     , type => {int, [{min, 0}]}
     , default => 10000 }.

count_arg() ->
    #{ name => count 
     , long => "-count"
     , type => boolean
     , help => "Return only the total count"
     , short => $c
     , default => false}.

peer_id_arg() ->
    #{ name => peer_id 
     , type => string
     , help => "The peer id"
     }.

peer_address_arg() ->
    #{ name => address
     , type => string
     , help => "The address of the peer: aenode://<peer pubkey>@<host>:<port>"
     }.

main(Args) ->
    Opts = aeu_ext_scripts:parse_opts(Args, spec(), #{progname => admin}),
    {ok, Node} = aeu_ext_scripts:connect_node(Opts),
    try
        #{cmd_spec := {_, #{handler := {Fun, _}}}, opts := ArgList} = Opts,
        Fun(Node, ArgList)
    catch E:R:StackTrace ->
        case {E, R} of
            {throw, {decode_error, Argument}} ->
                io:format(standard_error, "Decode error, the ~p is invalid~n",
                          [Argument]);
            {throw, {process_error, Msg}} ->
                io:format(standard_error, "~s~n", [Msg]);
            {error, _} ->
                io:format(standard_error, "~p:~p~n~p", [E, R, StackTrace])
        end,
        erlang:halt(1)
    end.

delete_tx_from_pool(Node, #{ 'tx_hash' := EncTxHash} = ArgsList) ->
    TxHash = decode(tx_hash, EncTxHash),
    case rpc(Node, aec_tx_pool, delete, [TxHash], ArgsList) of
        ok ->
            io:fwrite("Tx deleted from the pool.~n", []),
            erlang:halt(0);
        {error, not_found} -> throw({process_error, "Transaction not present in the tx-pool"});
        {error, already_accepted} -> throw({process_error, "Transaction already accepted"})
    end.

push_tx_to_pool(Node, #{ 'tx' := EncTx, 'force' := Force, 'timeout' := Timeout} = ArgsList) ->
    Tx = decode(transaction, EncTx),
    Push =
        fun() ->
            case Force of
                true ->
                    rpc(Node, aec_tx_pool, force_push, [Tx, Timeout], ArgsList);
                false ->
                    rpc(Node, aec_tx_pool, push, [Tx, tx_created, Timeout], ArgsList)
            end
        end,
    case Push() of
        ok ->
            io:fwrite("The transaction is being put in the pool.~n", []),
            erlang:halt(0);
        {error, Msg} -> throw({process_error, io_lib:format("Transaction push failed: ~p",
                                                            [Msg])})
    end.

inspect_tx_in_pool(Node, #{ 'tx_hash' := EncTxHash} = ArgsList) ->
    TxHash = decode(tx_hash, EncTxHash),
    case rpc(Node, aec_tx_pool, inspect, [TxHash], ArgsList) of
        {ok, Failures, Visited, TTL} ->
            io:fwrite("Failures: ~p~n"
                      "Visited : ~p~n"
                      "TTL     : ~p~n", [Failures, Visited, TTL]),
            erlang:halt(0);
        {error, not_found} -> throw({process_error, "Transaction not present in the tx-pool"});
        {error, already_accepted} -> throw({process_error, "Transaction already accepted"})
    end.

tx_pool_size(Node, #{ 'show' := "all"} = ArgsList) ->
    Cnt = rpc(Node, aec_tx_pool, size, [], ArgsList),
    io:fwrite("~p", [Cnt]),
    erlang:halt(0);
tx_pool_size(Node, #{ 'show' := "visited"} = ArgsList) ->
    Cnt = rpc(Node, aec_tx_pool, size, [visited], ArgsList),
    io:fwrite("~p", [Cnt]),
    erlang:halt(0);
tx_pool_size(Node, #{ 'show' := "not_visited"} = ArgsList) ->
    Cnt = rpc(Node, aec_tx_pool, size, [not_visited], ArgsList),
    io:fwrite("~p", [Cnt]),
    erlang:halt(0);
tx_pool_size(_Node, _ArgsList) ->
    io:fwrite("Not supported option for argument 'show': {all|visited|not_visited}\n", []),
    erlang:halt(1).

tx_pool_miner_gas_price(Node, ArgsList) ->
    Cnt = rpc(Node, aec_tx_pool, minimum_miner_gas_price, [], ArgsList),
    io:fwrite("~p", [Cnt]),
    erlang:halt(0).

set_tx_pool_miner_gas_price(Node, #{ 'new_gas_price' := NewGasPrice} = ArgsList) ->
    Config = #{<<"mining">> => #{ <<"min_miner_gas_price">> => NewGasPrice}},
    rpc(Node, aeu_env, update_config, [Config, _Notify = true, _InfoReport = silent], ArgsList),
    io:fwrite("Updated to ~p\n", [NewGasPrice]),
    erlang:halt(0).

rpc(Node, Mod, Fun, Args, #{'timeout' := Timeout}) ->
    rpc:call(Node, Mod, Fun, Args, Timeout).

decode(tx_hash, EncTxHash) ->
    case aeser_api_encoder:safe_decode(tx_hash, list_to_binary(EncTxHash)) of
        {ok, Hash} -> Hash;
        _ -> throw({decode_error, tx_hash})
    end;
decode(transaction, EncTx) ->
    case aeser_api_encoder:safe_decode(transaction, list_to_binary(EncTx)) of
        {ok, Bin} ->
            try
                aetx_sign:deserialize_from_binary(Bin)
            catch
                _:_ -> throw({decode_error, transaction})
            end;
        _Err ->
            throw({decode_error, transaction})
    end.

list_connected_peers(Node, #{ 'show' := Show} = ArgsList)
    when Show =:= "outbound";
         Show =:= "inbound";
         Show =:= "all" ->
    ShowAtom = list_to_atom(Show),
    Peers0 = rpc(Node, aec_peers, connected_peers, [ShowAtom], ArgsList),
    Peers = format_peers(Peers0, ArgsList),
    lists:foreach(
        fun(P) ->
            io:fwrite("~s\n", [P])
        end,
        Peers),
    erlang:halt(0);
list_connected_peers(_Node, _ArgsList) ->
    io:fwrite("Not supported option for argument 'show': {all|inbound|outbound}\n", []),
    erlang:halt(1).

list_verified_peers(Node, ArgsList) ->
    Peers0 = rpc(Node, aec_peers, available_peers, [verified], ArgsList),
    Peers = format_peers(Peers0, ArgsList),
    lists:foreach(
        fun(P) ->
            io:fwrite("~s\n", [P])
        end,
        Peers),
    erlang:halt(0).

list_unverified_peers(Node, ArgsList) ->
    Peers0 = rpc(Node, aec_peers, available_peers, [unverified], ArgsList),
    Peers = format_peers(Peers0, ArgsList),
    io:fwrite("~s\n", [Peers]),
    erlang:halt(0).

list_blocked_peers(Node, ArgsList) ->
    Peers0 = rpc(Node, aec_peers, blocked_peers, [], ArgsList),
    Peers = format_peers(Peers0, ArgsList),
    lists:foreach(
        fun(P) ->
            io:fwrite("~s\n", [P])
        end,
        Peers),
    erlang:halt(0).


format_peers(Peers, #{'count' := true}) ->
    [io_lib:format("~B", [length(Peers)])];
format_peers([_|_] = Peers0, #{'count' := false}) ->
    [format_peer_info(PI) || PI <- Peers0];
format_peers([], #{'count' := false}) ->
    [""].

format_peer_info(#{ pubkey  := Pubkey 
                  , host    := Host
                  , port    := Port}) ->
    io_lib:format("aenode://~s@~s:~B", [aeser_api_encoder:encode(peer_pubkey, Pubkey),
                               Host, Port]).

add_peer(Node, #{'address' := PeerAddress, 'trusted' := Trusted} = ArgsList) ->
    case aec_peers:parse_peer_address(PeerAddress) of
        {ok, PeerInfo} ->
            case Trusted of
                false ->
                    ok = rpc(Node, aec_peers, add_peers, [undefined, PeerInfo],
                             ArgsList);
                true ->
                    ok = rpc(Node, aec_peers, add_trusted, [PeerInfo],
                             ArgsList)
            end,
            io:fwrite("Ok.~n", []),
            erlang:halt(0);
        {error, _} ->
            throw({decode_error, address})
    end,
    ok.

remove_peer(Node, #{'peer_id' := EncPeerPubkey} = ArgsList) ->
    case aeser_api_encoder:safe_decode(peer_pubkey, list_to_binary(EncPeerPubkey)) of
        {ok, PeerPubkey} ->
            ok = rpc(Node, aec_peers, del_peer, [PeerPubkey],
                      ArgsList),
            io:fwrite("Ok.~n", []),
            erlang:halt(0);
        {error, _E} ->
            throw({decode_error, peer_id})
    end.

block_peer(Node, #{'address' := PeerAddress} = ArgsList) ->
    case aec_peers:parse_peer_address(PeerAddress) of
        {ok, PeerPubkey} ->
            ok = rpc(Node, aec_peers, block_peer, [PeerPubkey],
                      ArgsList),
            io:fwrite("Ok.~n", []),
            erlang:halt(0);
        {error, _E} ->
            throw({decode_error, address})
    end.

unblock_peer(Node, #{'peer_id' := EncPeerPubkey} = ArgsList) ->
    case aeser_api_encoder:safe_decode(peer_pubkey, list_to_binary(EncPeerPubkey)) of
        {ok, PeerPubkey} ->
            ok = rpc(Node, aec_peers, unblock_peer, [PeerPubkey],
                      ArgsList),
            io:fwrite("Ok.~n", []),
            erlang:halt(0);
        {error, _E} ->
            throw({decode_error, peer_id})
    end.

